跳到主要内容

Nest 中的设计思想

一、 MVC 架构概述

Nest 属于 MVC(Model-View-Controller,模型-视图-控制器)架构体系,实际上,大多数后端框架都是基于这一架构设计的。

在 MVC 架构中:

  • Model 层负责业务逻辑处理,包括数据的获取、存储、验证以及数据库操作。
  • Controller 层通常用于处理用户的输入,调度 Service 服务,以及进行 API 的路由管理。
  • View 层在传统的服务端渲染中,可能使用如 ejs、hbs 等模板引擎。在前后端分离的体系中,通常指的是客户端框架(如 Vue 或 React)负责的部分。

当一个 HTTP 请求到达服务器时,它首先会被 Controller 层接收。Controller 层会根据请求调用 Model 层中的相应模块来处理业务逻辑,并将处理结果返回给 View 层以进行展示。

二、AOP 思想

AOP(面向切面编程,Aspect-Oriented Programming)是一种编程范式,旨在通过分离关注点(cross-cutting concerns)来提高代码的模块化。它的核心思想是将不同功能(如日志记录、事务管理、权限检查等)从业务逻辑中分离出来,独立地进行管理和维护。这样,代码的可读性、可维护性和复用性都会得到提高。

在传统的面向对象编程(OOP)中,程序的关注点(功能)通常会混杂在一起,导致代码难以扩展和维护。而AOP通过引入“切面”(Aspect)这一概念,使得这些功能(如日志、性能监控、错误处理等)能够在不修改原有业务逻辑代码的情况下进行统一处理。

2.1 AOP的主要概念

  1. 切面(Aspect):切面是关注点的模块化,它定义了要在程序中何时、如何执行某些代码。比如,日志、事务等通常会是切面。

  2. 连接点(Join Point):程序执行过程中可以插入切面的点,比如方法调用、方法执行前后等。AOP会在这些连接点上插入增强代码。

  3. 通知(Advice):增强的代码,指在连接点执行的额外操作。通知有不同的类型:

    • 前置通知(Before):在目标方法执行之前执行。
    • 后置通知(After):在目标方法执行之后执行。
    • 环绕通知(Around):在目标方法执行前后都可以控制。
    • 异常通知(AfterThrowing):目标方法抛出异常时执行。
  4. 切入点(Pointcut):用于定义在哪些连接点(通常是方法调用)上执行通知。通过表达式来指定切入点。

  5. 织入(Weaving):将切面应用到目标对象的过程。织入可以发生在编译时、类加载时或者运行时。

2.2 AOP的优势

  • 代码解耦:将横切关注点(例如日志、权限验证)与核心业务逻辑分离,降低了耦合度。
  • 增强功能:可以在不修改原有业务代码的情况下增加额外的功能。
  • 可维护性高:分离了不同的功能模块,使代码更容易维护。

2.3 AOP在实际中的应用

  • 日志记录:每次方法调用前后自动记录日志。
  • 事务管理:自动在方法执行前开启事务,执行后提交或回滚。
  • 权限检查:在方法执行前自动检查权限。
  • 性能监控:记录方法的执行时间,分析性能瓶颈。

例如,Spring框架中的AOP就是非常典型的应用,它允许开发者在不修改业务逻辑代码的情况下,添加事务管理、日志记录等功能。

2.4 AOP在Nest中的应用

2.4.1 中间件

Nest的中间件默认是基于 Express 的。

中间件可以在路由处理程序之前或之后插入执行任务,分为全局中间件和局部中间件。中间件必须实现 NestModule 接口中的 configure 方法。

全局中间件通过 use 方法调用。所有进入应用的请求都会经过全局中间件,通常用于执行日志统计、监控、安全性处理等任务。

async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 全局中间件
app.use((new LoggerMiddleware()).use);
await app.listen(80);
}
void bootstrap();

局部中间件通常应用于特定的控制器或单个路由上,以实现更系粒度的逻辑控制。

export class UserModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
// 针对此模块的所有路由绑定中间件
// consumer.apply(LoggerMiddleware).forRoutes('*');
// 指定中间件的路由和请求方式
consumer.apply(LoggerMiddleware).forRoutes({
path: '/user',
method: RequestMethod.GET,
});
}
}

2.4.2 守卫

守卫通常用于权限、角色等授权操作。

守卫在调用路由程序之前返回 true 或 false 来判断是否通行,分为全局守卫和局部守卫。

守卫必须实现 CanActivate 接口中的 canActivate() 方法。

@Injectable()
export class PersonGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
// code
// 通常根据 ExecutionContext 信息来判断权限,返回 true/false
return true
}
}

全局守卫:

async function bootstrap() {
const app = await NestFactory.create(AppModule)
// 守卫
app.useGlobalGuards(new PersonGuard())
await app.listen(8888)
}

局部守卫可以缩小控制范围,实现更加精细的权限控制:

@Controller('person')
// 声明守卫
@UseGuards(new PersonGuard())
// 控制器
export class PersonController {}

2.4.3 拦截器

拦截器在路由请求之前和之后都可以进行逻辑处理,能够充分劲劲操作 request 和 response 对象。

拦截器通常用于记录请求日志、转换或格式化响应数据,分为全局作用域、控制器作用域、全局作用域。

拦截器必须实现 NestInterceptor 接口中的 intercept 方法。通常与 RxJS 异步处理库一起使用,以执行一些异步操作。

// 统计接口超时的拦截器
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { log } from 'console';
import { Observable, tap, timeout } from 'rxjs';

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> | Promise<Observable<any>> {
console.log('进入拦截器:', context.getClass());
const now = Date.now();
// 调用 handle
return next.handle().pipe(
tap(() => {
// 统计耗时
log('Timeout:', Date.now() - now);
}),
// 超时时间
timeout(1000),
);
}
}
2.4.3.1 控制器作用域
@Controller('person')
// 为控制器绑定超时拦截器
@UseInterceptors(new TimeoutInterceptor())

export class PersonController {}
2.4.3.2 方法作用域
@Get()
// 为单独的方法绑定超时拦截器
@UseInterceptors(new TimeoutInterceptor())
findAll() {
return this.personService.findAll()
}
2.4.3.3 全局作用域
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.useGlobalnterceptors(new TimeoutInterceptor())
await app.listen(8888)
}

2.4.4 管道

管道用于处理能用逻辑,例如处理请求参数的验证和转换。

2.4.4.1 内置管道
  1. ParseIntPipe:将将数据转换为整数类型;
  2. ParseFloatPipe:将数据转换为浮点数类型;
  3. ParseBoolPipe:将数据转换为布尔类型;
  4. ParseUUIDPipe:生成UUID;
  5. ParseEnumPipe:验证枚举值;
  6. DefaultValuePipe:指定默认值;
  7. ValidationPipe:验证POST请求参数;
  8. ParseArrayPipe:将字符串转换为数组类型;
  9. ParseFilePipe:文件上传验证。
2.4.4.2 自定义管道

自定义管道需要实现PipeTransform接口的transform方法。

import {
ArgumentMetadata,
BadRequestException,
Injectable,
PipeTransform,
} from '@nestjs/common';

@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 8);
if (isNaN(val)) {
throw new BadRequestException(
`Validation failed. "${value}" is not an integer.`,
);
}
return val;
}
}

2.4.5 过滤器

最为常见的是HTTP异常过滤器,用于后端服务发生异常时向客户端报告异常的类型。

2.4.5.1 内置HTTP异常
  1. BadRequestException:表示请求有误
  2. UnauthorizedException:表示未授权
  3. NotFoundException:表示未找到资源
  4. ForbiddenException:表示访问被拒绝
  5. NotAcceptableException:表示请求的内容类型不可接受
  6. RequestTimeoutException:表示请求超时
  7. ConflictException:表示请求冲突
  8. GoneException:表示资源已不可用
  9. HttpVersionNotSupportedException:表示不支持的HTTP版本
  10. PayloadTooLargeException:表示请求体过大
  11. UnsupportedMediaTypeException:表示不支持的媒体类型
2.4.5.2 自定义HTTP异常过滤器

...

2.4.5.2.1 将过滤器绑定到控制器

...

2.4.5.2.2 将过滤器绑定到某个路由方法中

...

2.4.5.2.3 将过滤器绑定到全局中

...

三、IoC 思想

IoC(Inversion of Control,控制反转)在 NestJS 中是核心设计原则之一。它通过内置的 依赖注入(Dependency Injection,DI)系统 实现了 IoC,让代码高度解耦、可测试和可维护。

3.1 核心思想

  • 传统方式:类自己负责创建和管理依赖(通过 new 或直接导入),导致紧耦合。
  • IoC 方式:控制权反转给 Nest 的 IoC 容器。类只声明需要什么依赖,容器负责创建、组装和注入(“别自己找,让容器给你送来”)。

NestJS 的 IoC 容器会在应用启动时自动解析所有依赖关系,创建单例(默认)或瞬态实例,并注入到需要的地方。

3.2 传统方式(无 IoC)的对比

假设我们有一个用户服务,需要依赖用户仓库(Repository)来操作数据库。

// user.repository.ts
export class UserRepository {
save() {
console.log('保存用户到数据库');
}
}

// user.service.ts(无 IoC,紧耦合)
import { UserRepository } from './user.repository';

export class UserService {
private repo: UserRepository;

constructor() {
this.repo = new UserRepository(); // 自己手动 new,耦合严重
}

saveUser() {
this.repo.save();
}
}

问题:如果 UserRepository 实现改变(比如换成 MongoDB),UserService 必须修改代码。测试时也很难 mock。

3.3 使用 NestJS 的 IoC(依赖注入)

NestJS 通过装饰器和模块系统实现 IoC。

  1. 标记可注入的 Provider(Service/Repository 等):
// user.repository.ts
import { Injectable } from '@nestjs/common';

@Injectable() // 关键:标记为可注入
export class UserRepository {
save() {
console.log('保存用户到数据库');
}
}
// user.service.ts
import { Injectable } from '@nestjs/common';
import { UserRepository } from './user.repository';

@Injectable()
export class UserService {
constructor(private readonly repo: UserRepository) {} // 通过构造函数声明依赖

saveUser() {
this.repo.save();
}
}
  1. 在模块中注册 Provider
// app.module.ts
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';

@Module({
providers: [UserService, UserRepository], // 注册到 IoC 容器
exports: [UserService], // 可选:导出供其他模块使用
})
export class AppModule {}
  1. 在 Controller 中使用(同样注入):
// user.controller.ts
import { Controller, Post } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('users')
export class UsersController {
constructor(private readonly userService: UserService) {} // 自动注入

@Post()
create() {
this.userService.saveUser();
return '用户已保存';
}
}

NestJS 的 IoC 容器会在应用启动时:

  • 扫描所有 @Injectable() 类。
  • 自动创建 UserRepository 实例。
  • 注入到 UserService 的构造函数。
  • 再注入 UserServiceUsersController

你完全不需要手动 new,容器全权负责。

3.4高级用法示例

  • 自定义 Provider:可以用 useClassuseValueuseFactory 等方式灵活配置。
@Module({
providers: [
{
provide: 'DATABASE', // 自定义 token
useFactory: () => new UserRepository(), // 工厂模式
},
],
})
  • 作用域控制:默认单例(Singleton),也可设为 REQUEST(每个请求新实例)或 TRANSIENT

3.5 IoC 在 NestJS 中的好处

  • 解耦:服务只依赖抽象(接口),易于替换实现(如换 ORM)。
  • 易测试:单元测试时可以用 Test.createTestingModule() 创建 mock 模块,注入假实现。
  • 模块化:大型项目中,每个模块独立管理自己的 providers,其他模块通过导入使用。
  • 自动解析:支持循环依赖检测、异步 provider 等高级特性。

总结:NestJS 把 IoC/DI 做得非常优雅和类型安全(得益于 TypeScript),是构建企业级后端应用的首选框架之一。如果你正在用 NestJS 开发,掌握 @Injectable() 和模块配置就是掌握了 IoC 的精髓。